﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using NeuralLib;

namespace NEATlib
{
    public class NeatAlgorithm
    {
        private readonly double _connectionDisableMutationProbability;
        private readonly double _connectionMutationProbability;
        private readonly double _distanceTreshold;
        private readonly int _generationCount;
        private readonly int _genomesCount;
        private readonly double[] _genomesFitness;
        private readonly double _neuronMutationProbability;
        private readonly int _newGenomesCount;

        private readonly Genom[] _newGenoms;
        private readonly NeuralNetworkInfo _nninfo;

        private readonly Genom[] _population;

        private readonly List<Specie> _species;
        private int _historyLabel;

        private readonly DataCollection _data;

        private Dictionary<double[], double[]> validationData; 

        public NeatAlgorithm(NeatParameters parameters, NeuralNetworkInfo nninfo, List<DataItem> dataItems, Dictionary<double[], double[]> validationData)
        {
            _genomesCount = parameters.GenomesCount;
            _newGenomesCount = (int) (_genomesCount*parameters.NewGenomesPercent);
            _newGenoms = new Genom[_newGenomesCount];
            _population = new Genom[_genomesCount];
            _genomesFitness = new double[_genomesCount];
            _historyLabel = 0;
            _distanceTreshold = parameters.DistanceTreshold;
            _nninfo = nninfo;
            _generationCount = parameters.GenerationCount;

            _neuronMutationProbability = parameters.NeuronMutationRate;
            _connectionMutationProbability = parameters.ConnectionMutationRate;
            _connectionDisableMutationProbability = parameters.ConnectionDisableMutationRate;
            _species = new List<Specie>();

            _data = new DataCollection(dataItems);

            this.validationData = validationData;

            //  _data = CancerDataProvider.GetCollection();
        }


        public event EventHandler OneStepPerformed;
        public event EventHandler Completed;

        public NeatResult Run()
        {
            Initialization();

            _historyLabel = _nninfo.InputCount;

            Niching(_population);

            
            PopulationFitnessCalculation();

            const double targetError = 0.05;

            double averaregError = 1.0;

            int round = 0;

           while (averaregError > targetError)
            {
                _historyLabel++;
                PerformNeatStep();
                if (OneStepPerformed != null)
                {
                   // int percent = (int) ((float)100 * 1 - (targetError / (averaregError - targetError)));

                    NeatResult result = GetBestResult();

                    averaregError = result.Error;

                    OneStepPerformed(null, new ProgressChangedEventArgs(round++, result));
                }
            }

            return GetBestResult();
        }

        private NeatResult GetBestResult()
        {
            double minError = 1;
            Genom bestPopulation = null;
            for (int i = 0; i < _genomesCount; i++)
            {
                double error = Test(_population[i]);
                if (error < minError)
                {
                    minError = error;
                    bestPopulation = _population[i];
                }
            }
 
            return new NeatResult(bestPopulation, minError);
        }

        private double Test(Genom bestSpecie)
        {
            DisconnectedNetwork network = NeuralNetworkConstructor.NetworkFromGenome(bestSpecie, _nninfo);


            double error = 0;

            foreach (KeyValuePair<double[], double[]> kvp in validationData)
            {
                double[] netResult = network.Compute(kvp.Key);
                int expected = GetmMaxElementIndex(kvp.Value);
                int actual = GetmMaxElementIndex(netResult);

                if (expected != actual)
                {
                    error++;
                }

            }

            return error / validationData.Count;

            //for (int i = 0; i < _data.SamplesCount; i++)
            //{
            //    double[] netResult = network.Compute(_data[i].DataSeries);
            //    double currentError = 0;
            //    for (int j = 0; j < netResult.Length; j++)
            //    {
            //        currentError += Math.Abs(_data[i].ExpectedOutput[j] - netResult[j]);
            //    }
            //    error += currentError / netResult.Length;

            //}
            //return error / _data.SamplesCount;
        }

        private int GetmMaxElementIndex(double[] items)
        {
            double max = -1;
            int maxIndex = -1;
            for (int i = 0; i < items.Length; i++)
            {
                if (items[i] > max)
                {
                    max = items[i];
                    maxIndex = i;
                }
            }

            return maxIndex;
        }

        private void PerformNeatStep()
        {
            Array.Clear(_newGenoms, 0, _newGenomesCount);

            PopulationCrossingover();

            Array.Sort(_genomesFitness, _population);

            for (int i = 0; i < _newGenomesCount; i++)
            {
                Genom genomToDelete = _population[_genomesCount - 1 - i];
                _species[genomToDelete.SpecieId].Genomes.Remove(genomToDelete);
                _population[_genomesCount - 1 - i] = _newGenoms[i];
            }

            PopulationMutation();

            Niching(_newGenoms);

            PopulationFitnessCalculation();
        }

        private void Niching(Genom[] newbies)
        {
            for (int i = 0; i < newbies.Count(); i++)
            {
                var newbie = newbies[i];
                if (AddToExistedSpecie(newbie)) continue;
                _species.Add(new Specie(newbie));
                newbie.SpecieId = _species.Count - 1;
            }
        }

        private bool AddToExistedSpecie(Genom newbie)
        {
            for (int j = 0; j < _species.Count; j++)
            {
                var specie = _species[j];
                var addToSpecie = specie.Genomes.All(specieGenom => !(Distance(newbie, specieGenom) > _distanceTreshold));
                if (addToSpecie)
                {
                    specie.Genomes.Add(newbie);
                    newbie.SpecieId = j;
                    return true;
                }
            }
            return false;
        }

        private void PopulationCrossingover()
        {
            for (int i = 0; i < _newGenomesCount; i++)
            {
                int parent1Id = Roulette.Int(0, _genomesCount - 1);
                int parent2Id;
                do
                {
                    parent2Id = Roulette.Int(0, _genomesCount - 1);
                } while (parent2Id == parent1Id);

                Genom newGenom = Crossingover(_population[parent1Id], _genomesFitness[parent1Id],
                    _population[parent2Id], _genomesFitness[parent2Id]);
                _newGenoms[i] = newGenom;
            }
        }

        private void PopulationFitnessCalculation()
        {
            for (int k = 0; k < _genomesCount; k++)
                _genomesFitness[k] = FitnessCalculation(_population[k]);
        }

        private void PopulationMutation()
        {
            foreach (Genom genom in _population)
            {
                double neuronMutationChance = Roulette.Double();
                double connectionMutationChance = Roulette.Double();
                double connectionDisableMutationChance = Roulette.Double();
                if (neuronMutationChance < _neuronMutationProbability)
                    NeuronMutation(genom);
                if (connectionMutationChance < _connectionMutationProbability)
                    AddConnectionMutation(genom);
                if (connectionDisableMutationChance < _connectionDisableMutationProbability)
                    ConnectionDisableMutation(genom);
            }
        }

        private double FitnessCalculation(Genom genom)
        {
            DisconnectedNetwork network = NeuralNetworkConstructor.NetworkFromGenome(genom, _nninfo);
            double error = 0;
            int outputLength = _data[0].ExpectedOutput.Length;
            for (int i = 0; i < _data.SamplesCount; i++)
            {
                double[] netResult = network.Compute(_data[i].DataSeries);
                for (int j = 0; j < outputLength; j++)
                    error += Math.Abs(_data[i].ExpectedOutput[j] - netResult[j]);
            }
            int specieSize = _species[genom.SpecieId].Genomes.Count/_genomesCount + 1;
            return error*specieSize/_data.SamplesCount;
        }

        private Genom Crossingover(Genom parent1, double parent1Fitness, Genom parent2, double parent2Fitness)
        {
            var newgenom = new Genom();
            for (int i = 1; i < _historyLabel; i++)
            {
                ConnectionGene connections1 = parent1.Connections.FirstOrDefault(item => item.InnovationNumber == i);
                ConnectionGene connections2 = parent2.Connections.FirstOrDefault(item => item.InnovationNumber == i);
                if (connections1 == null && connections2 == null)
                    continue;
                if (connections1 != null && connections2 != null)
                {
                    double averageWeight = connections1.Weight + connections2.Weight/2;
                    newgenom.Connections.Add(new ConnectionGene(connections1.InputNeuronId, connections1.OutputNeuronId,
                        averageWeight, i));
                }
                else if (Math.Abs(parent1Fitness - parent2Fitness) < 0.001)
                {
                    double random = Roulette.Double();
                    if (random < 0.5)
                    {
                        if (connections1 != null)
                            newgenom.Connections.Add((ConnectionGene) connections1.Clone());
                    }
                    else
                    {
                        if (connections2 != null)
                            newgenom.Connections.Add((ConnectionGene) connections2.Clone());
                    }
                }
                else
                {
                    if (parent1Fitness < parent2Fitness && connections1 != null)
                        newgenom.Connections.Add((ConnectionGene) connections1.Clone());
                    else if (parent2Fitness < parent1Fitness && connections2 != null)
                        newgenom.Connections.Add((ConnectionGene) connections2.Clone());
                }
            }
            newgenom.Neurons = parent1.Neurons.Count >= parent2.Neurons.Count
                ? parent1.Neurons.Clone()
                : parent2.Neurons.Clone();
            return newgenom;
        }

        private void ConnectionDisableMutation(Genom genom)
        {
            int connectionId = Roulette.Int(0, genom.Connections.Count - 1);
            genom.Connections[connectionId].IsDisabled = !genom.Connections[connectionId].IsDisabled;
        }

        private void AddConnectionMutation(Genom genom)
        {
            int mutationType;
            List<NeuronGene> inputNeurons = genom.Neurons.Where(n => n.Type == ENeuronType.NtInput).ToList();
            List<NeuronGene> outputNeurons = genom.Neurons.Where(n => n.Type == ENeuronType.NtOutput).ToList();
            List<NeuronGene> hiddenNeurons = genom.Neurons.Where(n => n.Type == ENeuronType.NtHidden).ToList();
            int inputNeuronId = 0, outputNeuronId = 0;

            int hiddenNeuronsCount = hiddenNeurons.Count();
            int inputNeuronsCount = inputNeurons.Count();
            int outputNeuronsCount = outputNeurons.Count();

            if (hiddenNeuronsCount == 0)
                mutationType = 1;
            else if (hiddenNeuronsCount > 1)
                mutationType = Roulette.Int(0, 3);
            else
                mutationType = Roulette.Int(0, 2);

            switch (mutationType)
            {
                case 0:
                {
                    //input to hidden
                    int inputNeuron = Roulette.Int(0, inputNeuronsCount - 1);
                    int hiddenNeuron = Roulette.Int(0, hiddenNeuronsCount - 1);
                    inputNeuronId = inputNeurons[inputNeuron].NeuronIndex;
                    outputNeuronId = hiddenNeurons[hiddenNeuron].NeuronIndex;

                    break;
                }
                case 1:
                {
                    //input to output
                    int inputNeuron = Roulette.Int(0, inputNeuronsCount - 1);
                    int outputNeuron = Roulette.Int(0, outputNeuronsCount - 1);
                    inputNeuronId = inputNeurons[inputNeuron].NeuronIndex;
                    outputNeuronId = outputNeurons[outputNeuron].NeuronIndex;
                    break;
                }
                case 2:
                {
                    //hidden to output
                    int hiddenNeuron = Roulette.Int(0, hiddenNeuronsCount - 1);
                    int outputNeuron = Roulette.Int(0, outputNeuronsCount - 1);
                    inputNeuronId = hiddenNeurons[hiddenNeuron].NeuronIndex;
                    outputNeuronId = outputNeurons[outputNeuron].NeuronIndex;
                    break;
                }
                case 3:
                {
                    //hidden to hidden
                    int hiddenNeuron1 = Roulette.Int(0, hiddenNeuronsCount - 1);
                    int hiddenNeuron2 = hiddenNeuron1;
                    while (hiddenNeuron2 == hiddenNeuron1 ||
                           hiddenNeurons[hiddenNeuron1].Level >= hiddenNeurons[hiddenNeuron2].Level)
                    {
                        hiddenNeuron2 = Roulette.Int(0, hiddenNeuronsCount - 1);
                    }
                    inputNeuronId = hiddenNeurons[hiddenNeuron1].NeuronIndex;
                    outputNeuronId = hiddenNeurons[hiddenNeuron2].NeuronIndex;
                    break;
                }
            }
            double weight = (Roulette.Double()-Roulette.Double())/2;
            genom.Connections.Add(new ConnectionGene(inputNeuronId, outputNeuronId, weight, _historyLabel));
            _historyLabel++;
        }


        private void NeuronMutation(Genom genom)
        {
            List<NeuronGene> outputNeurons = genom.Neurons.Where(n => n.Type == ENeuronType.NtOutput).ToList();
            List<ConnectionGene> outputConnections = genom.Connections
                .Where(c => outputNeurons.Any(n => n.NeuronIndex == c.OutputNeuronId)).ToList();

            int connectionNumber = Roulette.Int(0, outputConnections.Count);
            ConnectionGene selectedConnection = outputConnections[connectionNumber];
            selectedConnection.IsDisabled = true;
            int neuronLevel = genom.Neurons.First(n => n.NeuronIndex == selectedConnection.InputNeuronId).Level + 1;
            int neuronId = genom.Neurons.Count;
            genom.Neurons.Add(new NeuronGene(neuronId, ENeuronType.NtHidden, neuronLevel));
            genom.Connections.Add(new ConnectionGene(selectedConnection.InputNeuronId, neuronId,
                selectedConnection.Weight, _historyLabel));
            _historyLabel++;
            genom.Connections.Add(new ConnectionGene(neuronId, selectedConnection.OutputNeuronId,
                selectedConnection.Weight, _historyLabel));
            _historyLabel++;
        }

        private double Distance(Genom genom1, Genom genom2)
        {
            int lastInnovationNumber1 = genom1.Connections.Last().InnovationNumber;
            int lastInnovationNumber2 = genom2.Connections.Last().InnovationNumber;
            int excessGenesCount = 0,
                disjointGenesCount = 0,
                matchingGenesCount = 0,
                genom1GenesCount = 0,
                genom2GenesCount = 0;
            double summMatchingGenes = 0;

            for (int i = 1; i <= _historyLabel; i++)
            {
                ConnectionGene connections1 = genom1.Connections.FirstOrDefault(item => item.InnovationNumber == i);
                ConnectionGene connections2 = genom2.Connections.FirstOrDefault(item => item.InnovationNumber == i);

                if (connections1 == null && connections2 == null)
                    continue;

                if (connections1 != null && connections2 == null)
                {
                    genom1GenesCount++;
                    disjointGenesCount++;
                    if (i > lastInnovationNumber2)
                    {
                        excessGenesCount++;
                    }
                }
                else if (connections1 == null)
                {
                    genom2GenesCount++;
                    disjointGenesCount++;
                    if (i > lastInnovationNumber1)
                    {
                        excessGenesCount++;
                    }
                }
                else
                {
                    genom1GenesCount++;
                    genom2GenesCount++;
                    matchingGenesCount++;
                    summMatchingGenes += Math.Abs(connections1.Weight - connections2.Weight);
                }
            }
            int maxGenesCount = Math.Max(genom1GenesCount, genom2GenesCount);
            return (double) (excessGenesCount + disjointGenesCount)/maxGenesCount +
                   0.4*summMatchingGenes/matchingGenesCount;
        }

        private void Initialization()
        {
            for (int i = 0; i < _genomesCount; i++)
            {
                _population[i] = GenomesCreator.GetInitialGenom(_nninfo);
            }
        }
    }

    internal static class Extensions
    {
        public static List<T> Clone<T>(this List<T> listToClone) where T : ICloneable
        {
            return listToClone.Select(item => (T) item.Clone()).ToList();
        }
    }

  
    public class NeatResult
    {
        public NeatResult(Genom genom, double error)
        {
            BestGenom = genom;
            Error = error;
        }

        public Genom BestGenom { get; internal set; }
        public double Error { get; internal set; }
    }
}